﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Text.RegularExpressions;
using System.Web;
using SelArom.Net.Sitefinity.API.Events.Data;
using SelArom.Net.Sitefinity.Helpers;
using Telerik.Events;
using Telerik.Events.Configuration;
using Newtonsoft.Json;
using System.Net;

namespace SelArom.Net.Sitefinity.API.Events.Data
{
	/// <summary>
	/// Wrapper class for an IEvent item
	/// </summary>
	public class EventDetails : IComparable
	{
		public EventDetails() { }

		/// <summary>
		/// Initializes a new instance of the <see cref="EventDetails"/> class from an existing IEvent item.
		/// </summary>
		/// <param name="sEvent">IEvent item from which to copy data.</param>
		/// <param name="InitContent">if set to <c>true</c>, initializes the full content from the ContentItem.</param>
		public EventDetails(IEvent sEvent, bool InitContent)
		{
			Init(sEvent, InitContent);
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="EventDetails"/> class from an existing IEvent item. DOES NOT initialize full ContentItem data, only summary.
		/// </summary>
		/// <param name="sEvent">The IEvent from which to copy data.</param>
		public EventDetails(IEvent sEvent)
		{
			Init(sEvent, false);
		}

		/// <summary>
		/// Initializes data from an existing IEvent item.
		/// </summary>
		/// <param name="sEvent">IEvent item from which to copy data.</param>
		/// <param name="InitContent">if set to <c>true</c>, initializes the full content from the ContentItem.</param>
		private void Init(IEvent sEvent, bool InitContent)
		{
			// copy IEvent data into new item
			ID = sEvent.ID.ToString();
			EventTitle = HttpUtility.HtmlEncode(sEvent.EventTitle.Replace("\"", "'"));

			Category = sEvent.ContentItem.GetMetaData("Category").ToString();
			Start = sEvent.Start;
			End = sEvent.End;
			Content = InitContent ? HttpUtility.HtmlEncode(sEvent.ContentItem.Content.ToString()) : string.Empty;
			Source = "Internal";

			// create summary from content
			var cnt = Content;
			cnt = Regex.Replace(cnt, "<[^>]*>", "");
			Summary = cnt.Length > 50 ? cnt.Remove(50) + " ..." : cnt;

			// ensure url appkey is present, initialize full url
			var evUrl = ConfigurationManager.AppSettings[EventsManager.DefaultContentProvider + "Url"];
			var url = string.IsNullOrEmpty(evUrl) ? sEvent.ContentItem.UrlWithExtension : string.Concat(evUrl, sEvent.ContentItem.UrlWithExtension);
			Url = string.Concat("http://", HttpContext.Current.Request.Url.Host, url.ToLower());

			// optional Location item
			var loc = sEvent.ContentItem.GetMetaData("Location");
			Location = loc == null ? string.Empty : loc.ToString();
		}

		/// <summary>
		/// Gets or sets the uniqe ID for the event.
		/// </summary>
		/// <value>The Event ID.</value>

		public string ID { get; set; }

		/// <summary>
		/// Gets or sets the event title.
		/// </summary>
		/// <value>The event title.</value>

		public string EventTitle { get; set; }

		/// <summary>
		/// Gets or sets the start date and time for the event.
		/// </summary>
		/// <value>The event start date and time.</value>

		public DateTime Start { get; set; }

		/// <summary>
		/// Gets or sets the end date and time for the event.
		/// </summary>
		/// <value>The event end date and time.</value>

		public DateTime End { get; set; }

		/// <summary>
		/// Gets or sets the event category.
		/// </summary>
		/// <value>The event category.</value>

		public string Category { get; set; }

		/// <summary>
		/// Gets or sets the source (Internal, Library, Quinta, etc) of the event.
		/// </summary>
		/// <value>The event source (website).</value>

		public string Source { get; set; }

		/// <summary>
		/// Gets or sets the full event content (description).
		/// </summary>
		/// <value>The event description content.</value>

		public string Content { get; set; }

		/// <summary>
		/// Gets or sets the event summary.
		/// </summary>
		/// <value>The event summary.</value>

		public string Summary { get; set; }

		/// <summary>
		/// Gets or sets the event URL.
		/// </summary>
		/// <value>The event URL.</value>

		public string Url { get; set; }

		/// <summary>
		/// Gets or sets the event location.
		/// </summary>
		/// <value>The event location.</value>

		public string Location { get; set; }

		#region IComparable Members
		public int CompareTo(object obj)
		{
			var ev = obj as EventDetails;
			if (ev == null)
				throw new ArgumentException("Argument is not the correct type");
			return Start.CompareTo(ev.Start);
		}
		#endregion
	}

	/// <summary>
	/// Holds a collection of EventDetails objects in an array whose indexes correspond to the day of the month (index 0 is empty).
	/// </summary>
	public class MonthEvents
	{
		public MonthEvents()
		{
			Days = new EventList[32];
			for (var i = 0; i < 32; i++)
				Days[i] = new EventList();
		}

		/// <summary>
		/// Sorts each day's corresponding list by event start date.
		/// </summary>
		public void Sort()
		{
			for (var i = 0; i < Days.Length; i++)
				Days[i].Sort();
		}
		/// <summary>
		/// An array of EventLists where each index of the array corresponds to the day of the month, each day containing an EventList of items taking place that day
		/// </summary>

		public EventList[] Days;

		private int count = -1;
		/// <summary>
		/// Total number of events in the current month
		/// </summary>
		/// <value>The total number of events in the month.</value>

		public int Count
		{
			get
			{
				if (count == -1)
					for (var i = 1; i < Days.Length; i++)
						count += Days[i].Count;
				return count;
			}
		}
	}

	/// <summary>
	/// A collection of EventDetails items
	/// </summary>
	public class EventList : List<EventDetails>
	{

	}
}

namespace SelArom.Net.Sitefinity.API.Events
{
	/// <summary>
	/// Class for retrieving data from the Default Data Provider for Sitefinity Events Module.
	/// </summary>
	public class EventsAPI
	{
		protected EventsManager mgr;
		protected HttpServerUtility Server;
		private const string CACHE_LOCATION = "~/App_Data/API/events";

		/// <summary>
		/// Initializes a new instance of the <see cref="EventsAPI"/> class.
		/// </summary>
		public EventsAPI()
		{
			Server = HttpContext.Current.Server;
			var handler = (SectionHandler)ConfigurationManager.GetSection("telerik/events");
			mgr = new EventsManager(handler.DefaultProvider);
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="EventsAPI"/> class.
		/// </summary>
		/// <param name="ProviderName">Name of the provider.</param>
		public EventsAPI(string ProviderName)
		{
			mgr = new EventsManager(ProviderName);
		}

		/// <summary>
		/// Erases cached files for a specific date
		/// </summary>
		/// <param name="Date">The date's cache to clear.</param>
		public void Clear(DateTime Date)
		{
			CacheHelper.Delete(string.Format("{0}/MonthEvents{1}{2:D2}.json", CACHE_LOCATION, Date.Year, Date.Month));
			MonthEvents(Date.Year, Date.Month);

			CacheHelper.Delete(string.Format("{0}/ExternalMonthEvents{1}{2:D2}.json", CACHE_LOCATION, Date.Year, Date.Month));
			ExternalMonthEvents(Date.Year, Date.Month);
		}

		#region FromExternalApi
		/// <summary>
		/// Retreive MonthEvents from a specific external website API
		/// </summary>
		/// <param name="Url">The url to the external website api call.</param>
		/// <returns>MonthEventCounts result from the API call</returns>
		public MonthEvents MonthEvents(string Url)
		{
			// initialize empty set
			var events = new MonthEvents();

			// ensure url exists
			if (string.IsNullOrEmpty(Url))
				throw new ArgumentNullException("Url cannot be blank");

			try
			{
				// create request
				var webRequest = WebRequest.Create(Url) as HttpWebRequest;
				if (webRequest == null) return null;

				// browser detect requires user agent!
				webRequest.UserAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)";

				// on error return empty set
				var response = webRequest.GetResponse() as HttpWebResponse;
				if (response == null) return null;

				// parse response
				using (var sr = new StreamReader(response.GetResponseStream()))
				{
					var jsonResponse = sr.ReadToEnd();
					events = JsonConvert.DeserializeObject<MonthEvents>(jsonResponse);
				}
			}
			catch (Exception ex)
			{
				// todo: log error
			}
			
			// received null once, not sure why, put this in case it happens again
			if (events.Days == null) events = new MonthEvents();

			return events;
		}


		#endregion

		#region LocalEvents
		/// <summary>
		/// Returns a list of daily events collections hashed by day for a given month
		/// </summary>
		/// <param name="Year">The selected year.</param>
		/// <param name="Month">The selected month.</param>
		/// <returns>MonthEvents containing the collection of events by day</returns>
		public MonthEvents MonthEvents(int Year, int Month)
		{
			var month = new MonthEvents();

			// create CACHE_LOCATION file if required
			var path = string.Format("{0}/MonthEvents{1}{2:D2}.json", CACHE_LOCATION, Year, Month);
			var file = new FileInfo(Server.MapPath(path));
			var cache = CacheHelper.ReadFile(file);

			// check CACHE and refresh if necessary
			if (string.IsNullOrEmpty(cache))
			{
				// search the full month
				var Start = new DateTime(Year, Month, 1);
				var End = new DateTime(Year, Month, DateTime.DaysInMonth(Year, Month), 23, 59, 59);

				// get local events
				var events = mgr.GetEvents(Start, End, "[Start] ASC");
				foreach (IEvent sEvent in events)
				{
					// skip non-published items
					if (sEvent.ContentItem.Status != Telerik.Cms.Engine.ContentStatus.Published) continue;

					// init and add the event to the collection
					var ev = new EventDetails(sEvent);
					month.Days[ev.Start.Day].Add(ev);
				}

				// write to cache
				cache = JsonConvert.SerializeObject(month);
				CacheHelper.WriteFile(file.FullName, cache);
			}
			else
				month = JsonConvert.DeserializeObject<MonthEvents>(cache);

			return month;
		}

		/// <summary>
		/// Retrieves a list of events for the selected day
		/// </summary>
		/// <param name="SelectedDate">The selected date.</param>
		/// <returns>EventsList instance for the given day</returns>
		public EventList EventsByDate(DateTime SelectedDate)
		{
			// retrieve day from month cache
			var monthEvents = MonthEvents(SelectedDate.Year, SelectedDate.Month);
			var events = monthEvents.Days[SelectedDate.Day];
			return events;
		}

		/// <summary>
		/// Retrieves from the cache the next coming events (up to 21 days in the future), up to a count of specified Max
		/// </summary>
		/// <param name="Max">The maximum number of items to retrieve.</param>
		/// <returns>EventList containing the number of items</returns>
		public EventList UpcomingEvents(int Max)
		{
			// init
			var events = new EventList();
			var now = DateTime.Now;

			// inc date until full
			for (var i = 0; i < 21 && events.Count < Max; i++)
				events.AddRange(EventsByDate(now.AddDays(i)));

			// remove excess and return
			if (events.Count > Max) events.RemoveRange(Max, events.Count - Max);
			return events;
		}
		#endregion


		#region ExternalEvents
		/// <summary>
		/// Returns a list of daily events collections hashed by day for a given month flagged to show on external websites
		/// </summary>
		/// <param name="Year">The selected year.</param>
		/// <param name="Month">The selected month.</param>
		/// <returns>MonthEvents containing the collection of external events by day</returns>
		public MonthEvents ExternalMonthEvents(int Year, int Month)
		{
			var month = new MonthEvents();

			// create CACHE_LOCATION file if required
			var path = string.Format("{0}/ExternalMonthEvents{1}{2:D2}.json", CACHE_LOCATION, Year, Month);
			var file = new FileInfo(Server.MapPath(path));
			var cache = CacheHelper.ReadFile(file);

			// check CACHE and refresh if necessary
			if (string.IsNullOrEmpty(cache))
			{
				// search the full month
				var numdays = DateTime.DaysInMonth(Year, Month);
				var Start = new DateTime(Year, Month, 1);
				var End = new DateTime(Year, Month, numdays, 23, 59, 59);

				// get local events
				var events = mgr.GetEvents(Start, End, "[Start] ASC");
				foreach (IEvent sEvent in events)
				{
					// skip non-published items
					if (sEvent.ContentItem.Status != Telerik.Cms.Engine.ContentStatus.Published) continue;

					var external = sEvent.ContentItem.GetMetaData("External");
					if (external == null || string.IsNullOrEmpty(external.ToString()) || !(bool)external) continue;

					// init and add the event to the collection
					var ev = new EventDetails(sEvent, true);
					month.Days[ev.Start.Day].Add(ev);
				}

				// write to cache
				cache = JsonConvert.SerializeObject(month);
				CacheHelper.WriteFile(file.FullName, cache);
			}
			else
				// CACHE_LOCATION still valid
				month = JsonConvert.DeserializeObject<MonthEvents>(cache);

			return month;
		}

		/// <summary>
		/// Retrieves a list of events for the selected day
		/// </summary>
		/// <param name="SelectedDate">The selected date.</param>
		/// <returns>EventsList instance for the given day</returns>
		public EventList ExternalEventsByDate(DateTime SelectedDate)
		{
			// retrieve day from month cache
			var monthEvents = ExternalMonthEvents(SelectedDate.Year, SelectedDate.Month);
			var events = monthEvents.Days[SelectedDate.Day];
			return events;
		}
		#endregion
	}
}